---
title: Server middleware
description: Learn how to create middleware that runs for every request to the server in Expo Router.
---

import { Collapsible } from '~/ui/components/Collapsible';
import { Terminal } from '~/ui/components/Snippet';
import { Step } from '~/ui/components/Step';

> **important** Server middleware is an experimental feature available in SDK 54 and later, and requires a [deployed server](/router/web/api-routes/#deployment) for production use.

Server middleware in Expo Router allows you to run code before requests reach your routes, enabling powerful server-side functionality like authentication and logging for every request. Unlike [API routes](/router/web/api-routes) that handle specific endpoints, middleware runs for **every** request in your app, so it should run as quickly as possible to avoid slowing down your app's performance. Client-side navigation such as on native, or in a web app when using [`<Link />`](/versions/latest/sdk/router/#link), will not move through the server middleware.

## Setup

<Step label="1">

### Enable server middleware in your app configuration

First, configure your app to use server output by adding the server configuration to your [app config](/versions/latest/config/app/):

```json app.json
{
  "expo": {
    /* @hide ... */
    /* @end */
    "web": {
      /* @info Middleware is only supported in server mode */
      "output": "server"
      /* @end */
    },
    "plugins": [
      [
        "expo-router",
        {
          /* @info Enables server middleware */
          "unstable_useServerMiddleware": true
          /* @end */
        }
      ]
    ]
  }
}
```

</Step>

<Step label="2">

### Create your middleware file

Create a **+middleware.ts** file in your **app** directory, to define your server middleware function:

```ts app/+middleware.ts
export default function middleware(request) {
  console.log(`Middleware executed for: ${request.url}`);
  // Your middleware logic goes here
}
```

The middleware function must be the default export of the file. It receives an [immutable request](#request-immutability) and can return either a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response), or nothing to let the request pass through unmodified. The request is immutable to prevent side effects; you can read headers and properties, but you cannot modify headers or consume the request body.

</Step>

<Step label="3">

### Start your development server

Run your development server to test the middleware:

<Terminal cmd={['$ npx expo start']} />

Your middleware will now run for all requests to your app.

</Step>

<Step label="4">

### Test middleware functionality

Visit your app in a browser or make requests to test that your middleware is working. Check your console for the log messages from the middleware function.

</Step>

<Step label="5">

### Configure middleware matchers (optional)

By default, middleware runs on all server requests. You can add a matcher to control when your middleware executes with `unstable_settings`:

```ts app/+middleware.ts
export const unstable_settings = {
  matcher: {
    // Only run on GET requests
    methods: ['GET'],
    // Only run on API routes and specific paths
    patterns: ['/api', '/admin/[...path]'],
  },
};

export default function middleware(request) {
  console.log(`Middleware executed for: ${request.url}`);
}
```

The matcher configuration allows you to:

- **Filter by HTTP method**: Specify which methods should trigger the middleware
- **Filter by path patterns**: Define which URL patterns should match using exact paths, named parameters, or regular expressions

</Step>

## How it works

Middleware functions are executed before any route handlers, allowing you to perform actions like logging, authentication, or modifying responses. It runs exclusively on the server and only for actual HTTP requests.

### Request/response flow

When a request comes to your app, Expo Router processes it in this order:

1. The middleware function runs first with an [immutable request](#request-immutability).
2. If middleware returns a `Response`, that response is sent immediately
3. If middleware returns nothing, the request continues to the matching route
4. The route handler processes the request and returns its response

### Pattern matching

Matchers support different pattern types to control when middleware runs:

```ts
export const unstable_settings = {
  matcher: {
    patterns: [
      '/api', // Exact path
      '/posts/[postId]', // Named parameter
      '/blog/[...slug]', // Catch-all parameter
      /^\/api\/v\d+\/users$/, // Regular expression
    ],
  },
};
```

- **Exact paths** match only the specified path. `/api` matches `/api` but not `/api/users`
- **Named parameters** like `[postId]` capture any single segment. `/posts/[postId]` matches `/posts/123` or `/posts/my-post`
- **Catch-all parameters** like `[...slug]` capture one or more segments. `/blog/[...slug]` matches `/blog/2024` or `/blog/2024/12/post`
- **Regular expressions** for complex patterns. `/^\/api\/v\d+\/users$/` matches `/api/v1/users` but not `/api/users`

Middleware runs if **any** pattern matches the request URL. When both `methods` and `patterns` are specified, both conditions must be met for middleware to run.

### Middleware execution order

Expo Router supports a single middleware file named **+middleware.ts** that runs for all server requests. When using matchers, middleware executes only for requests that match the specified patterns and methods, before any route matching or rendering occurs.

### When middleware runs

Middleware executes only for actual HTTP requests to your server. This means it is executed for:

- Initial page loads, like when a user first visits your site
- Full page refreshes
- Direct URL navigation
- API route calls from any client (native/web apps, external services)
- Server-side rendering requests

Middleware does not run for:

- Client-side navigation using [`<Link />`](/versions/latest/sdk/router/#link) or [`router`](/versions/latest/sdk/router/#router)
- Native app screen transitions
- Prefetched routes
- Static asset requests like images and fonts

## Examples

<Collapsible summary="Authentication">

Middleware is often used to perform authorization checks before a route has loaded. You can check headers, cookies, or query parameters to determine if a user has access to certain routes:

```ts app/+middleware.ts
import { jwtVerify } from 'jose';

export default function middleware(request) {
  const token = request.headers.get('authorization');

  const decoded = jwtVerify(token, process.env.SECRET_KEY);
  if (!decoded.payload) {
    return new Response('Forbidden', { status: 403 });
  }
}
```

</Collapsible>

<Collapsible summary="Logging">

You can use middleware to log requests for debugging or analytics purposes. This can help you track user activity or diagnose issues in your app:

```ts app/+middleware.ts
export default function middleware(request) {
  console.log(`${request.method} ${request.url}`);
}
```

</Collapsible>

<Collapsible summary="Dynamic redirects">

Middleware can also be used to perform dynamic redirects. This allows you to control user navigation based on specific conditions:

```ts app/+middleware.ts
export default function middleware(request) {
  if (request.headers.has('specific-header')) {
    return Response.redirect('https://expo.dev');
  }
}
```

</Collapsible>

<Collapsible summary="API-only middleware">

Use matchers to run middleware only for API routes, keeping other routes unaffected:

```ts app/+middleware.ts
export const unstable_settings = {
  matcher: {
    patterns: ['/api'],
  },
};

export default function middleware(request) {
  // Log all API requests for debugging
  console.log(`API request: ${request.method} ${request.url}`);

  // Add CORS headers for API routes
  const response = new Response();
  response.headers.set('Access-Control-Allow-Origin', '*');
  return response;
}
```

</Collapsible>

<Collapsible summary="Method-specific authentication">

Protect write operations (POST, PUT, DELETE) while allowing public read access:

```ts app/+middleware.ts
export const unstable_settings = {
  matcher: {
    methods: ['POST', 'PUT', 'DELETE'],
    patterns: ['/api', '/admin/[...path]'],
  },
};

export default function middleware(request) {
  const token = request.headers.get('authorization');

  if (!token || !isValidToken(token)) {
    return new Response('Unauthorized', { status: 401 });
  }
}

function isValidToken(token: string): boolean {
  // Your token validation logic
  return token.startsWith('Bearer ');
}
```

</Collapsible>

<Collapsible summary="Selective logging">

Monitor specific endpoints without logging every request:

```ts app/+middleware.ts
export const unstable_settings = {
  matcher: {
    patterns: ['/api/users/[userId]', '/admin', /^\/webhook/],
  },
};

export default function middleware(request) {
  const userAgent = request.headers.get('user-agent');
  const timestamp = new Date().toISOString();

  console.log(`[${timestamp}] ${request.method} ${request.url} - ${userAgent}`);
}
```

</Collapsible>

## Additional notes

### Best practices

- Keep middleware lightweight because it runs synchronously on every server request and directly impacts response times.
- Use matchers to optimize performance by avoiding unnecessary middleware execution on routes that don't need it, especially for high-traffic applications.
- Prefer exact paths and named parameters over regex as simple patterns are faster to evaluate and easier to maintain than complex regular expressions.
- Combine method and pattern filtering for precise control over when middleware executes.
- For native apps, use API routes for secure data fetching. When native apps call API routes, those requests will pass through middleware first.

### Typed middleware

```ts app/+middleware.ts
import { MiddlewareFunction } from 'expo-router/server';

const middleware: MiddlewareFunction = request => {
  if (request.headers.has('specific-header')) {
    return Response.redirect('https://expo.dev');
  }
};

export default middleware;
```

### Limitations

- Middleware runs exclusively on the server and only for HTTP requests. It does not execute during client-side navigation, for example, with [`<Link />`](/versions/latest/sdk/router/#link) or native app screen transitions.
- The request object passed to middleware is [immutable](#request-immutability) to prevent side effects. You cannot modify headers or consume the request body, ensuring it remains available for route handlers.
- You can only have one root-level **+middleware.ts** in your app.
- The same limitations that [apply to API routes](/router/web/api-routes/#known-limitations) also apply to middleware.

### Request immutability

To prevent unintended side effects and ensure the request body remains available for route handlers, the [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) passed to middleware is immutable. This means you can:

- Read all request properties like `url`, `method`, `headers`, and so on
- Read header values using `request.headers.get()`
- Check for header existence with `request.headers.has()`
- Access URL parameters and query strings

But you won't be able to:

- Modify headers with `set()`, `append()`, `delete()`
- Consume the request body with `text()`, `json()`, `formData()`, and so on
- Access the `body` property directly
